import re
import json
import hashlib
import shutil
import tempfile
import subprocess
import sys
import os
import argparse
import logging
import random
import time
import signal
import threading

from datetime import datetime
from typing import Optional, List, Dict, Set
from importlib.metadata import distribution

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 全局超时处理
class TimeoutException(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutException("操作超时")

def set_timeout(seconds, handler=timeout_handler):
    """跨平台超时处理：Linux/Mac 用 signal，Windows 用 threading"""
    if hasattr(signal, 'SIGALRM'):
        # Linux/Mac 用 SIGALRM
        signal.signal(signal.SIGALRM, handler)
        signal.alarm(seconds)
        return None
    else:
        # Windows 没有 SIGALRM，用 Timer
        timer = threading.Timer(seconds, lambda: handler())
        timer.daemon = True
        timer.start()
        return timer

def cancel_timeout(timer):
    """取消超时"""
    if timer is not None:
        timer.cancel()
    elif hasattr(signal, 'SIGALRM'):
        signal.alarm(0)  # 取消Linux/Mac的警报

class PyPiInstaller:
    """自动化PyPI包安装工具"""
    
    DEFAULT_MIRRORS = [
        "https://mirrors.cloud.tencent.com/pypi/simple",  # 腾讯
        "https://pypi.tuna.tsinghua.edu.cn/simple",       # 清华
        "https://mirrors.aliyun.com/pypi/simple",         # 阿里
    ]

    def __init__(self, 
                 dry_run: bool = False, 
                 verbose: bool = False, 
                 use_uv: bool = True,
                 max_retries: int = 3,
                 timeout_base: int = 30,
                 cache_dir: str = "./cache"):
        self.dry_run = dry_run
        self.verbose = verbose
        self.use_uv = use_uv

        self.cache_dir = cache_dir
        self.cached_packages: Dict[str, str] = {}
        self.installed_packages: Set[str] = set()
        self.failed_packages: List[str] = []
        self.skipped_packages: List[str] = []

        self.max_retries = max_retries
        self.timeout_base = timeout_base
        
        # 添加详细状态记录
        self.repo_results: Dict[str, Dict] = {}  # repo_name -> {status, packages, results}
        self.package_details: Dict[str, Dict] = {}  # package -> {repo, status, error_msg}

        # 镜像队列
        self.mirrors = list(self.DEFAULT_MIRRORS)
        self.mirror_status: Dict[str, Dict] = {}  # mirror -> {success_count, failure_count, last_used}

        # 初始化缓存目录
        self._init_cache_dir()
        
        # 检查uv是否可用
        if self.use_uv:
            self._check_uv_available()
    
    def _init_cache_dir(self):
        """初始化缓存目录"""
        os.makedirs(self.cache_dir, exist_ok=True)
        os.makedirs(os.path.join(self.cache_dir, "wheels"), exist_ok=True)
        os.makedirs(os.path.join(self.cache_dir, "metadata"), exist_ok=True)
        
        # 加载缓存元数据
        self._load_cache_metadata()
    
    def _load_cache_metadata(self):
        """加载缓存元数据"""
        metadata_file = os.path.join(self.cache_dir, "metadata", "cache.json")
        if os.path.exists(metadata_file):
            try:
                with open(metadata_file, 'r') as f:
                    self.cached_packages = json.load(f)
            except Exception as e:
                logger.warning(f"加载缓存元数据失败: {e}")
    
    def _save_cache_metadata(self):
        """保存缓存元数据"""
        metadata_file = os.path.join(self.cache_dir, "metadata", "cache.json")
        try:
            with open(metadata_file, 'w') as f:
                json.dump(self.cached_packages, f, indent=2)
        except Exception as e:
            logger.error(f"保存缓存元数据失败: {e}")

    def _check_package_in_cache(self, package: str) -> Optional[str]:
        """检查包是否在缓存中"""
        package_hash = hashlib.md5(package.encode()).hexdigest()
        cached_path = os.path.join(self.cache_dir, "wheels", f"{package_hash}.whl")
        
        if package in self.cached_packages and os.path.exists(cached_path):
            return cached_path
        
        return None

    def _add_to_cache(self, package: str, wheel_path: str):
        """添加包到缓存"""
        package_hash = hashlib.md5(package.encode()).hexdigest()
        cached_path = os.path.join(self.cache_dir, "wheels", f"{package_hash}.whl")
        
        try:
            if os.path.exists(wheel_path):
                shutil.copy2(wheel_path, cached_path)
                self.cached_packages[package] = cached_path
                self._save_cache_metadata()
                logger.info(f"已缓存包: {package}")
        except Exception as e:
            logger.warning(f"缓存包失败: {package} - {e}")

    def _get_best_mirror(self) -> str:
        """选择最佳镜像源"""
        if not self.mirror_status:
            return self.mirrors[0]
        
        # 按成功率排序
        sorted_mirrors = sorted(
            self.mirror_status.items(),
            key=lambda x: (
                x[1].get('success_count', 0) / 
                max(1, x[1].get('success_count', 0) + x[1].get('failure_count', 0)),
                x[1].get('last_used', 0)
            ),
            reverse=True
        )
        
        return sorted_mirrors[0][0] if sorted_mirrors else self.mirrors[0]
    
    def _update_mirror_status(self, mirror: str, success: bool):
        """更新镜像源状态"""
        if mirror not in self.mirror_status:
            self.mirror_status[mirror] = {'success_count': 0, 'failure_count': 0}
        
        if success:
            self.mirror_status[mirror]['success_count'] += 1
        else:
            self.mirror_status[mirror]['failure_count'] += 1
        
        self.mirror_status[mirror]['last_used'] = time.time()

    def _check_uv_available(self):
        """检查uv是否可用"""
        try:
            subprocess.run(['uv', '--version'], check=True, capture_output=True)
            logger.info("✓ 检测到uv，将使用uv进行包管理")
        except (subprocess.CalledProcessError, FileNotFoundError):
            logger.warning("✗ 未检测到uv，回退到pip")
            self.use_uv = False

    def _get_install_command(self, package: str, mirror: str) -> List[str]:
        """获取安装命令"""
        if self.use_uv:
            # 全局uv安装
            return ['uv', 'pip', 'install', '--system', '--index-url', mirror, package]
        else:
            return [sys.executable, '-m', 'pip', 'install', '--index-url', mirror, package]

    def parse_results_file(self, file_path: str) -> Dict[str, Dict[str, any]]:
        """解析results.txt文件，提取包名和URL"""
        repo_data = {}
        
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                    
                parts = line.split(' ', 2)
                if len(parts) < 3:
                    continue
                    
                repo_name = parts[0]
                url = parts[1]
                packages_str = parts[2]
                
                # 解析包列表
                packages = self._parse_package_list(packages_str)
                repo_data[repo_name] = {
                    'url': url,
                    'packages': packages
                }
                
        return repo_data
    
    def _parse_package_list(self, packages_str: str) -> List[str]:
        """解析包列表字符串"""
        # 移除最外层的方括号（如果存在）
        packages_str = packages_str.strip()
        if packages_str.startswith('[') and packages_str.endswith(']'):
            packages_str = packages_str[1:-1]
        
        if not packages_str:
            return []
            
        # 分割包名 - 需要考虑包名中可能包含方括号（如 openbb[all]）
        packages = []
        current_package = ""
        bracket_count = 0
        
        for char in packages_str + ',':  # 添加逗号确保最后一个包被处理
            if char == '[':
                bracket_count += 1
                current_package += char
            elif char == ']':
                bracket_count -= 1
                current_package += char
            elif char == ',' and bracket_count == 0:
                # 只有在不在方括号内时才分割
                pkg = current_package.strip()
                if pkg and not self._should_skip_package(pkg):
                    packages.append(pkg)
                current_package = ""
            else:
                current_package += char
                
        return packages
    
    def _should_skip_package(self, package: str) -> bool:
        """判断是否应该跳过某个包"""
        skip_patterns = [
            'ERROR:',        # 错误
            'README_NOT_FOUND', # README未找到
            'INVALID_',      # 无效格式
            'git:',          # git安装
            'requirements:', # requirements文件
            'LocalInstall:', # 本地安装
        ]
        
        return any(pattern in package for pattern in skip_patterns)
    
    def _clean_package_name(self, package: str) -> str:
        """清理包名，移除版本号和其他修饰符"""
        # 移除版本约束 (==, >=, <=, >, <, !=)
        clean_name = re.split(r'[=<>!]+', package)[0]
        
        # 移除方括号内的extras (如 package[extra])
        clean_name = re.split(r'\[', clean_name)[0]
        
        # 移除空格和其他特殊字符
        clean_name = clean_name.strip()
            
        return clean_name.lower()
    
    def _verify_installation(self, package: str) -> bool:
        """验证包是否安装成功"""
        clean_name = self._clean_package_name(package)
        
        # 尝试导入包
        try:
            distribution(clean_name)
            logger.info(f"✓ 验证成功: {clean_name}")
            return True
        except ImportError as e:
            logger.warning(f"导入验证失败: {clean_name} - {e}")
            # 不是所有包都能直接导入，这不一定表示安装失败
            return True  # 仍然返回True，因为pip可能已成功安装
        except Exception as e:
            logger.warning(f"验证异常: {clean_name} - {e}")
            return True  # 不因验证失败而标记安装失败

    def _download_with_retry(self, package: str, mirror: str) -> bool:
        """使用重试机制下载包"""
        for attempt in range(self.max_retries):
            try:
                logger.info(f'尝试{attempt+1}/{self.max_retries} 下载 {package}')

                # 下载包到临时位置
                with tempfile.NamedTemporaryFile(suffix='.whl', delete=False) as tmp_file:
                    if self.use_uv:
                        cmd = ['uv', 'pip', 'install', '--index-url', mirror, 
                              '--target', os.path.dirname(tmp_file.name), package]
                    else:
                        cmd = [sys.executable, '-m', 'pip', 'download', '--index-url', mirror
                              , os.path.dirname(tmp_file.name), package]
                    
                    # 设置超时
                    timer = set_timeout(self.timeout_base)

                    result = subprocess.run(
                        cmd, 
                        capture_output=not self.verbose, 
                        text=True, 
                        timeout=self.timeout_base-3  # 比信号超时稍短
                    )
                    
                    cancel_timeout(timer)  # 取消超时
                    
                    if result.returncode == 0:
                        # 查找下载的wheel文件
                        download_dir = os.path.dirname(tmp_file.name)
                        for file in os.listdir(download_dir):
                            if file.endswith('.whl'):
                                wheel_path = os.path.join(download_dir, file)
                                self._add_to_cache(package, wheel_path)
                                return True

                    # 指数退避
                    sleep_time = (2 ** attempt) + (random.randint(0, 1000) / 1000)
                    logger.warning(f"下载失败，{sleep_time:.2f}秒后重试: {result.stderr}")
                    time.sleep(sleep_time)
                    
            except TimeoutException:
                logger.warning(f"下载超时，尝试 {attempt+1}/{self.max_retries}")
                cancel_timeout(timer)  # 取消超时
                continue
            except Exception as e:
                logger.warning(f"下载异常: {e}")
                cancel_timeout(timer)  # 取消超时
                continue
        
        return False

    def install_package(self, package: str, repo_name: str = None) -> bool:
        """安装单个包"""
        clean_package = self._clean_package_name(package)
        
        # 跳过特殊类型的包
        if self._should_skip_package(package):
            logger.info(f"跳过包: {package}")
            self.skipped_packages.append(package)
            self._record_package_result(package, repo_name, "skipped", "Package type not supported")
            return False

        # 检查是否已安装
        if clean_package in self.installed_packages:
            logger.info(f"包 {package} 已安装，跳过")
            self._record_package_result(package, repo_name, "skipped", "Already installed")
            return True

        # 检查缓存
        cached_path = self._check_package_in_cache(package)
        if cached_path and not self.dry_run:
            logger.info(f"使用缓存安装: {package}")
            try:
                if self.use_uv:
                    cmd = ['uv', 'pip', 'install', '--system', cached_path]
                else:
                    cmd = [sys.executable, '-m', 'pip', 'install', cached_path]
                
                subprocess.run(cmd, check=True, capture_output=not self.verbose, timeout=self.timeout_base)
                self.installed_packages.add(clean_package)
                logger.info(f"✓ 成功从缓存安装: {package}")
                self._record_package_result(package, repo_name, "success", None)
                return True
            except Exception as e:
                logger.warning(f"缓存安装失败，尝试正常安装: {e}")

        # 标准pip安装
        if self.dry_run:
            logger.info(f"[DRY RUN] 将安装: {package} {'(使用uv)' if self.use_uv else ''}")
            self._record_package_result(package, repo_name, "dry_run", "Dry run mode")
            return True

        # 尝试多个镜像源
        success = False
        error_msgs = []

        for attempt in range(self.max_retries):
            mirror = self._get_best_mirror()
            logger.info(f"尝试 {attempt+1}/{self.max_retries} 从 {mirror} 安装 {package}")
            try:
                cmd = self._get_install_command(package, mirror)
                logger.info(f"安装包: {package} {'(使用uv)' if self.use_uv else ''}")
                
                # 设置超时
                timer = set_timeout(self.timeout_base)

                if self.verbose:
                    result = subprocess.run(
                        cmd, 
                        check=True, 
                        capture_output=False, 
                        text=True, 
                        timeout=self.timeout_base - 3
                        )
                    cancel_timeout(timer)  # 取消超时
                else:
                    result = subprocess.run(cmd, check=True, capture_output=True, text=True)
                    if result.stderr:
                        logger.warning(f"安装警告: {result.stderr.strip()}")

                # 验证安装
                if self._verify_installation(package):
                    self.installed_packages.add(clean_package)
                    logger.info(f"✓ 成功安装: {package}")
                    self._record_package_result(package, repo_name, "success", None)
                    self._update_mirror_status(mirror, True)

                    # 尝试下载并缓存wheel
                    try:
                        self._download_with_retry(package, mirror)
                    except Exception as e:
                        logger.warning(f"缓存下载失败: {e}")

                    success = True
                    break
                else:
                    error_msg = "安装验证失败"
                    logger.warning(f"安装验证失败: {package}")
                    error_msgs.append(error_msg)
                    self._update_mirror_status(mirror, False)

            except subprocess.CalledProcessError as e:
                cancel_timeout(timer)
                error_msg = f"Installation failed: {e.stderr if e.stderr else str(e)}"
                logger.error(f"✗ 安装失败: {package} - {error_msg}")
                error_msgs.append(error_msg)
                self._update_mirror_status(mirror, False)
            except TimeoutException:
                cancel_timeout(timer)
                error_msg = "安装超时"
                logger.error(f"✗ 安装超时: {package}")
                error_msgs.append(error_msg)
                self._update_mirror_status(mirror, False)
            except Exception as e:
                cancel_timeout(timer)
                error_msg = f"安装异常: {str(e)}"
                logger.error(f"✗ 安装异常: {package} - {error_msg}")
                error_msgs.append(error_msg)
                self._update_mirror_status(mirror, False)
            
            # 指数退避
            if attempt < self.max_retries - 1:
                sleep_time = (2 ** attempt) + (random.randint(0, 1000) / 1000)
                logger.info(f"{sleep_time:.2f}秒后重试...")
                time.sleep(sleep_time)

        if not success:
            logger.error(f"✗ 最终安装失败: {package}")
            self.failed_packages.append(package)
            self._record_package_result(package, repo_name, "failed", "; ".join(error_msgs))
            return False
        
        return True

    def _record_package_result(self, package: str, repo_name: str, status: str, error_msg: str = None):
        """记录包安装结果"""
        self.package_details[package] = {
            'repo': repo_name,
            'status': status,
            'error_msg': error_msg,
            'timestamp': datetime.now().isoformat()
        }

    def install_from_repo(self, repo_name: str, repo_data: Dict[str, any], 
                         max_packages: int = None) -> Dict[str, bool]:
        """安装指定仓库的包"""
        logger.info(f"\n=== 处理仓库: {repo_name} ===")
        
        packages = repo_data['packages']
        repo_url = repo_data['url']
        
        # 初始化仓库结果记录
        self.repo_results[repo_name] = {
            'url': repo_url,
            'total_packages': len(packages),
            'processed_packages': 0,
            'successful': [],
            'failed': [],
            'skipped': [],
            'dry_run': [],
            'start_time': datetime.now().isoformat()
        }
        
        if not packages:
            logger.info("无可安装的包")
            self.repo_results[repo_name]['status'] = 'no_packages'
            return {}
        
        # 限制安装包数量
        if max_packages and len(packages) > max_packages:
            packages = packages[:max_packages]
            logger.info(f"限制安装前 {max_packages} 个包")
            
        results = {}
        for package in packages:
            success = self.install_package(package, repo_name)
            results[package] = success
            
            # 更新仓库统计
            self.repo_results[repo_name]['processed_packages'] += 1
            if package in self.package_details:
                status = self.package_details[package]['status']
                if status == 'success':
                    self.repo_results[repo_name]['successful'].append(package)
                elif status == 'failed':
                    self.repo_results[repo_name]['failed'].append(package)
                elif status == 'skipped':
                    self.repo_results[repo_name]['skipped'].append(package)
                elif status == 'dry_run':
                    self.repo_results[repo_name]['dry_run'].append(package)
        
        self.repo_results[repo_name]['end_time'] = datetime.now().isoformat()
        return results

    def _print_summary(self):
        """打印安装总结"""
        logger.info(f"\n{'='*50}")
        logger.info("安装总结:")
        
        # 统计仓库类型
        # 真实安装
        repos_without_packages = 0
        repos_all_success      = 0
        repos_partial_success  = 0
        repos_all_failed       = 0
        real_total_repos       = 0

        # Dry-Run
        repos_all_dryrun        = 0
        repos_partial_dryrun    = 0
        repos_all_dryrun_failed = 0
        total_dryrun_pkgs       = 0
        
        for repo_data in self.repo_results.values():
            total_pkgs = repo_data['total_packages']
            success_count = len(repo_data.get('successful', []))
            failed_count  = len(repo_data.get('failed', []))
            dryrun_items  = repo_data.get('dry_run', []) or []
            dryrun_count  = len(dryrun_items)
            total_dryrun_pkgs += dryrun_count
            status        = repo_data.get('status')
            dryrun_failed_count = len(repo_data.get('dry_run_failed', []))

            # ---------- Dry-Run 分类（与真实安装解耦） ----------
            if total_pkgs == 0 or status == 'no_packages':
                # 没检测到可安装包：按需求计为 Dry-Run 失败
                repos_all_dryrun_failed += 1
            elif 0 < dryrun_count < total_pkgs:
                repos_partial_dryrun += 1
            elif dryrun_count == total_pkgs:
                # 全部 dry-run 过：若无 dry-run 失败记录则全部成功，否则全部失败
                if dryrun_failed_count == 0:
                    repos_all_dryrun += 1
                else:
                    repos_all_dryrun_failed += 1
            else:
                # dryrun_count == 0 但 total_pkgs > 0 等异常情况，按失败处理
                repos_all_dryrun_failed += 1

            # ---------- 真实安装分类（仅当真的尝试过安装） ----------
            has_real_attempt = (success_count + failed_count) > 0
            if has_real_attempt:
                real_total_repos += 1
                if total_pkgs == 0 or status == 'no_packages':
                    repos_without_packages += 1
                elif failed_count == 0 and success_count > 0:
                    repos_all_success += 1
                elif success_count > 0 and failed_count > 0:
                    repos_partial_success += 1
                elif failed_count == total_pkgs and total_pkgs > 0:
                    repos_all_failed += 1
                else:
                    # 未能明确归类的真实尝试，计入“无包可安装”兜底
                    repos_without_packages += 1

        # ========= 真实安装总结（只看真实尝试过的仓库） =========
        repos_with_packages = repos_all_success + repos_partial_success + repos_all_failed
        logger.info("\n[真实安装结果]")
        logger.info(f"处理仓库总数: {real_total_repos}")
        logger.info(f"  ├─ 有包可安装: {repos_with_packages}")
        logger.info(f"  │   ├─ 全部成功: {repos_all_success}")
        logger.info(f"  │   ├─ 部分成功: {repos_partial_success}")
        logger.info(f"  │   └─ 全部失败: {repos_all_failed}")
        logger.info(f"  └─ 无包可安装: {repos_without_packages}")
        logger.info(f"成功安装: {len(self.installed_packages)} 个包")
        logger.info(f"安装失败: {len(self.failed_packages)} 个包")
        logger.info(f"跳过包数: {len(self.skipped_packages)} 个包")
        logger.info(f"缓存包数: {len(self.cached_packages)} 个包")

        # ========= Dry-Run 总结（覆盖所有仓库） =========
        logger.info("\n[Dry-Run 结果]")
        logger.info(f"处理仓库总数: {len(self.repo_results)}")
        logger.info(f"  ├─ 全部Dry-Run成功: {repos_all_dryrun}")
        logger.info(f"  ├─ 全部Dry-Run失败: {repos_all_dryrun_failed}")
        logger.info(f"  ├─ 部分Dry-Run: {repos_partial_dryrun}")
        logger.info(f"  └─ Dry-Run 包数: {total_dryrun_pkgs}")
        
        # 镜像源状态报告
        logger.info("镜像源状态:")
        for mirror, status in self.mirror_status.items():
            total = status.get('success_count', 0) + status.get('failure_count', 0)
            if total > 0:
                success_rate = status.get('success_count', 0) / total * 100
                logger.info(f"  {mirror}: {success_rate:.1f}% 成功率 ({status.get('success_count', 0)}/{total})")

        if self.failed_packages:
            logger.info("\n失败的包:")
            for pkg in self.failed_packages[:10]:  # 只显示前10个失败的包
                logger.info(f"  - {pkg}")
            if len(self.failed_packages) > 10:
                logger.info(f"  ... 还有 {len(self.failed_packages) - 10} 个失败的包")
                
        if self.installed_packages:
            logger.info(f"\n成功安装的包数量: {len(self.installed_packages)}")
            if len(self.installed_packages) <= 20:
                logger.info(f"成功安装的包: {', '.join(sorted(self.installed_packages))}")
            else:
                logger.info("成功安装的包太多，不在此显示详细列表")

    def install_all(self, repo_data: Dict[str, Dict[str, any]], 
                   target_repos: List[str] = None,
                   max_packages_per_repo: int = None) -> None:
        """批量安装所有包"""
        total_repos = len(repo_data)
        processed = 0
        
        for repo_name, data in repo_data.items():
            if target_repos and repo_name not in target_repos:
                continue
                
            processed += 1
            logger.info(f"\n[{processed}/{total_repos}] 处理仓库: {repo_name}")
            
            self.install_from_repo(repo_name, data, max_packages_per_repo)
            
        self._print_summary()

def main():
    parser = argparse.ArgumentParser(description="自动化PyPI包安装工具")
    parser.add_argument("--results_file", default='./2025实战任务_作品文件夹/OpenCloudOS 9 AI软件自动化验证工具/张耀翔_作品/pipeline_output/results_20250820_173954.txt', help="results.txt文件路径")
    parser.add_argument("--dry-run", action="store_true", help="仅显示将要安装的包，不实际安装")
    parser.add_argument("--verbose", "-v", action="store_true", help="显示详细安装过程")
    parser.add_argument("--repos", nargs="+", help="指定要处理的仓库名称")
    parser.add_argument("--max-per-repo", type=int, help="每个仓库最多安装的包数量")
    parser.add_argument("--upgrade", action="store_true", help="升级已安装的包")
    parser.add_argument("--no-uv", action="store_true", help="不使用uv，回退到pip")
    parser.add_argument("--cache-dir", default="./2025实战任务_作品文件夹/OpenCloudOS 9 AI软件自动化验证工具/张耀翔_作品/cache", help="缓存目录")
    parser.add_argument("--timeout", type=int, default=300, help="统一超时时间(秒)")
    parser.add_argument("--max-retries", type=int, default=5, help="最大重试次数")
    parser.add_argument("--max-packages", type=int, help="每个仓库最大安装包数")
    args = parser.parse_args()
    
    if not os.path.exists(args.results_file):
        logger.error(f"文件不存在: {args.results_file}")
        sys.exit(1)
    
    installer = PyPiInstaller(
        dry_run=True, 
        verbose=args.verbose, 
        use_uv=not args.no_uv,
        cache_dir=args.cache_dir,
        max_retries=args.max_retries,
        timeout_base=args.timeout
    )
    
    # 解析文件
    logger.info(f"解析文件: {args.results_file}")
    repo_data = installer.parse_results_file(args.results_file)
    logger.info(f"发现 {len(repo_data)} 个仓库")
    
    # 执行安装
    installer.install_all(
        repo_data=repo_data,
        target_repos=args.repos,
        max_packages_per_repo=args.max_per_repo
    )

if __name__ == "__main__":
    main()
